Прогнозирование спроса с использованием моделей временных рядов¶

In [10]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from prophet import Prophet
import seaborn as sns
from sklearn.metrics import mean_absolute_error, mean_squared_error
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.arima.model import ARIMA
import plotly.express as px

Загрузка и предварительная обработка данных¶

In [11]:
# Загрузка данных
df = pd.read_csv('/home/s/Code/jupyter/02_store_item_demand_forecasting/input_data/train.csv')
In [12]:
# Просмотр первых нескольких строк данных
df.head()
Out[12]:
date store item sales
0 2013-01-01 1 1 13
1 2013-01-02 1 1 11
2 2013-01-03 1 1 14
3 2013-01-04 1 1 13
4 2013-01-05 1 1 10
In [13]:
# Общая информация о данных
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 913000 entries, 0 to 912999
Data columns (total 4 columns):
 #   Column  Non-Null Count   Dtype 
---  ------  --------------   ----- 
 0   date    913000 non-null  object
 1   store   913000 non-null  int64 
 2   item    913000 non-null  int64 
 3   sales   913000 non-null  int64 
dtypes: int64(3), object(1)
memory usage: 27.9+ MB
In [14]:
# Преобразование столбца 'date' в формат datetime
df['date'] = pd.to_datetime(df['date'])
In [15]:
# Уменьшение памяти за счёт преобразования типов данных
df['sales'] = df['sales'].astype('int32')
df['store'] = df['store'].astype('category')
df['item'] = df['item'].astype('category')
In [16]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 913000 entries, 0 to 912999
Data columns (total 4 columns):
 #   Column  Non-Null Count   Dtype         
---  ------  --------------   -----         
 0   date    913000 non-null  datetime64[ns]
 1   store   913000 non-null  category      
 2   item    913000 non-null  category      
 3   sales   913000 non-null  int32         
dtypes: category(2), datetime64[ns](1), int32(1)
memory usage: 12.2 MB
In [17]:
# Проверка на наличие пропущенных значений
df.isnull().sum()
Out[17]:
date     0
store    0
item     0
sales    0
dtype: int64
In [18]:
# Основные статистические характеристики данных
df.describe(include='all')
Out[18]:
date store item sales
count 913000 913000.0 913000.0 913000.000000
unique NaN 10.0 50.0 NaN
top NaN 1.0 1.0 NaN
freq NaN 91300.0 18260.0 NaN
mean 2015-07-02 11:59:59.999999744 NaN NaN 52.250287
min 2013-01-01 00:00:00 NaN NaN 0.000000
25% 2014-04-02 00:00:00 NaN NaN 30.000000
50% 2015-07-02 12:00:00 NaN NaN 47.000000
75% 2016-10-01 00:00:00 NaN NaN 70.000000
max 2017-12-31 00:00:00 NaN NaN 231.000000
std NaN NaN NaN 28.801144

Анализ временных рядов с ARIMA¶

In [20]:
# Группировка данных по дате
daily_sales = df.groupby('date')['sales'].sum().asfreq('D')
In [21]:
# Модель ARIMA
model_arima = ARIMA(daily_sales, order=(5,1,0))
model_arima_fit = model_arima.fit()
forecast_arima = model_arima_fit.get_forecast(steps=90)
forecast_arima_ci = forecast_arima.conf_int()
forecast_arima_index = pd.date_range(start=daily_sales.index[-1] + pd.Timedelta(days=1), periods=90)
In [22]:
# Визуализация прогноза ARIMA
plt.figure(figsize=(14, 7))
plt.plot(daily_sales, label='Наблюдаемые', color='blue')
plt.plot(forecast_arima_index, forecast_arima.predicted_mean, label='Прогноз', color='red')
plt.fill_between(forecast_arima_index,
                 forecast_arima_ci.iloc[:, 0],
                 forecast_arima_ci.iloc[:, 1], color='red', alpha=0.3)
plt.legend()
plt.title('Прогноз ARIMA', fontsize=16)
plt.xlabel('Дата', fontsize=14)
plt.ylabel('Продажи', fontsize=14)
plt.grid(True)
plt.axvline(x=daily_sales.index[-1], color='black', linestyle='--')
plt.show()
No description has been provided for this image

Декомпозиция временных рядов¶

In [13]:
# Декомпозиция временного ряда
decomposition = seasonal_decompose(daily_sales, model='additive')
In [14]:
# Настройка стиля и визуализации декомпозиции
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(14, 10), sharex=True)

ax1.plot(decomposition.observed, label='Наблюдаемые', color='blue')
ax1.legend(loc='upper left')
ax1.set_ylabel('Продажи')

ax2.plot(decomposition.trend, label='Тренд', color='green')
ax2.legend(loc='upper left')
ax2.set_ylabel('Тренд')

ax3.plot(decomposition.seasonal, label='Сезонность', color='orange')
ax3.legend(loc='upper left')
ax3.set_ylabel('Сезонность')

ax4.plot(decomposition.resid, label='Остаток', color='purple')
ax4.legend(loc='upper left')
ax4.set_ylabel('Остаток')
ax4.set_xlabel('Дата')

plt.suptitle('Декомпозиция временного ряда', fontsize=16)
plt.show()
No description has been provided for this image

Прогнозирование спроса с Prophet¶

In [15]:
# Подготовка данных для Prophet
prophet_df = daily_sales.reset_index().rename(columns={'date': 'ds', 'sales': 'y'})
In [16]:
# Создание и обучение модели Prophet
model_prophet = Prophet()
model_prophet.fit(prophet_df)
02:05:19 - cmdstanpy - INFO - Chain [1] start processing
02:05:19 - cmdstanpy - INFO - Chain [1] done processing
Out[16]:
<prophet.forecaster.Prophet at 0x729b8b748c50>
In [17]:
# Прогнозирование на квартал
future_prophet = model_prophet.make_future_dataframe(periods=90)
forecast_prophet = model_prophet.predict(future_prophet)
In [18]:
# Визуализация прогноза Prophet вручную
fig, ax = plt.subplots(figsize=(14, 7))
ax.plot(prophet_df['ds'], prophet_df['y'], 'k.', label='Наблюдаемые')
ax.plot(forecast_prophet['ds'], forecast_prophet['yhat'], 'r-', label='Прогноз')
ax.fill_between(forecast_prophet['ds'], forecast_prophet['yhat_lower'], forecast_prophet['yhat_upper'], color='red', alpha=0.3)
ax.axvline(x=daily_sales.index[-1], color='black', linestyle='--', label='Начало прогноза')
ax.set_title('Прогноз Prophet', fontsize=16)
ax.set_xlabel('Дата', fontsize=14)
ax.set_ylabel('Продажи', fontsize=14)
plt.legend()
plt.show()
No description has been provided for this image

Сравнение методов¶

In [23]:
# Разделение данных на обучающие и тестовые наборы для оценки точности
train = daily_sales.iloc[:-90]
test = daily_sales.iloc[-90:]
In [24]:
# Прогнозирование с ARIMA
model_arima = ARIMA(train, order=(5,1,0))
model_arima_fit = model_arima.fit()
forecast_arima = model_arima_fit.forecast(steps=90)
In [25]:
# Прогнозирование с Prophet
prophet_df = train.reset_index().rename(columns={'date': 'ds', 'sales': 'y'})
model_prophet = Prophet()
model_prophet.fit(prophet_df)
future_prophet = model_prophet.make_future_dataframe(periods=90)
forecast_prophet = model_prophet.predict(future_prophet)

# Выбор последних 90 дней из прогноза Prophet
forecast_prophet_test = forecast_prophet.set_index('ds').loc[test.index]['yhat']
02:30:19 - cmdstanpy - INFO - Chain [1] start processing
02:30:19 - cmdstanpy - INFO - Chain [1] done processing
In [26]:
# Оценка точности ARIMA и Prophet
mae_arima = mean_absolute_error(test, forecast_arima)
rmse_arima = np.sqrt(mean_squared_error(test, forecast_arima))
mae_prophet = mean_absolute_error(test, forecast_prophet_test)
rmse_prophet = np.sqrt(mean_squared_error(test, forecast_prophet_test))

print(f'ARIMA MAE: {mae_arima}, RMSE: {rmse_arima}')
print(f'Prophet MAE: {mae_prophet}, RMSE: {rmse_prophet}')
ARIMA MAE: 4501.768381606831, RMSE: 5422.566804873452
Prophet MAE: 1115.6539490878185, RMSE: 1525.7773911382196
In [27]:
# Определение самой точной модели
if mae_arima < mae_prophet:
    print(f'Самая точная модель: ARIMA с MAE: {mae_arima} и RMSE: {rmse_arima}')
else:
    print(f'Самая точная модель: Prophet с MAE: {mae_prophet} и RMSE: {rmse_prophet}')
Самая точная модель: Prophet с MAE: 1115.6539490878185 и RMSE: 1525.7773911382196

Анализ по категориям товаров и магазинам¶

Визуализация суммарных продаж по каждому магазину¶

In [28]:
store_sales = df.groupby('store', observed=False)['sales'].sum().reset_index()

plt.figure(figsize=(14, 8))
sns.barplot(x='store', y='sales', hue='store', data=store_sales, palette=sns.color_palette("dark"), dodge=False)
plt.title('Суммарные продажи по магазинам', fontsize=16)
plt.xlabel('Магазин', fontsize=14)
plt.ylabel('Суммарные продажи', fontsize=14)
plt.xticks(rotation=45)
plt.ticklabel_format(style='plain', axis='y')  # Отображение оси Y в обычном формате
plt.grid(True)
plt.legend().set_visible(False)  # Убираем легенду
plt.show()
No description has been provided for this image

Визуализация продаж товаров в одном из магазинов (например, магазин 1)¶

In [29]:
store_item_sales = df[df['store'] == 1].groupby('item', observed=False)['sales'].sum().reset_index()

store_item_sales['item'] = store_item_sales['item'].astype(str)

fig = px.bar(store_item_sales, x='item', y='sales', title='Продажи товаров в магазине 1', labels={'item':'Товар', 'sales':'Суммарные продажи'}, template='plotly_dark')
fig.update_traces(marker_color='rgb(100,149,237)', marker_line_color='rgb(8,48,107)', marker_line_width=1.5)  # Используем более солидные цвета
fig.update_layout(title_font_size=16, xaxis_title_font_size=14, yaxis_title_font_size=14)
fig.show()
In [ ]:
 
In [ ]: